Design Task Management System

Ashish

Ashish Pratap Singh

easy

In this chapter, we will explore the low-level design of a task management system in detail.

Lets start by clarifying the requirements:

1. Clarifying Requirements

Before starting the design, it's important to ask thoughtful questions to uncover hidden assumptions, clarify ambiguities, and define the system's scope more precisely.

Here is an example of how a discussion between the candidate and the interviewer might unfold:

After gathering the details, we can summarize the key system requirements.

1.1 Functional Requirements

  1. Users can create, update, and delete tasks
  2. Each task should support metadata such as title, description, due date, priority, status, and tags
  3. Tasks can have subtasks, supporting a parent-child relationship
  4. Tasks can be grouped under task lists or projects (e.g., "Project Phoenix Bugs," "Q4 Marketing Plan").
  5. Users can assign tasks to themselves or others (single assignee per task)
  6. Users can update the status of a task (e.g., from 'To-Do' to 'In Progress').
  7. Users should be able to view tasks in a list sorted by due date or priority.
  8. Users can search tasks by title and filter tasks using keywords, status, user, etc.
  9. The system should maintain an activity log for each task to track creation, updates, and status changes

1.2 Non-Functional Requirements

  • Consistency: Task updates (e.g., status changes, reassignments) should be reflected immediately and reliably across the system
  • Scalability: The system should scale to support a large number of users, tasks, and projects
  • Maintainability: The codebase should follow good object-oriented principles to ensure modularity, testability, and ease of future development
  • Extensibility: The design should be flexible enough to accommodate future enhancements such as recurring tasks, notifications, file attachments, or role-based permissions

2. Identifying Core Entities

Core entities are the fundamental building blocks of our system. We identify them by analyzing the functional requirements and highlighting the key nouns and responsibilities that naturally map to object-oriented abstractions such as classes, enums, or interfaces.

Let’s walk through the functional requirements and extract the relevant entities:

1. Users can create, update, delete, and assign tasks to themselves or others.

This clearly indicates the need for a User entity to represent each participant in the system. The Task entity will be the central unit of work.

2. Each task contains metadata such as title, description, due date, priority, status, and tags.

This metadata maps directly to fields within the Task entity.

  • Status and Priority are constrained to a fixed set of values and can be modeled as enums (TaskStatus, TaskPriority).
  • Tags can either be simple strings or a separate Tag entity if we want more control (e.g., filtering or tag management).

3. Tasks can be grouped under a project or task list.

This suggests the need for a Project (or TaskList) entity to act as a container for related tasks. Each task will reference the project it belongs to.

4. The system should maintain an activity log for each task.

This indicates the need for an ActivityLog entity, which records events like task creation, status changes, and assignment updates. Each log entry will reference the task it belongs to and include metadata like timestamp and action type.

These core entities define the essential abstractions of a Task Management System and will guide the structure of our low-level design and class diagrams.

3. Class Design

3.1 Class Definitions

The system is broken down into several classes, each with a specific responsibility. They can be categorized as Enums, Data Classes, and Core Classes.

Enums

Enums
  • TaskStatus: Represents the distinct stages in a task's lifecycle (TODO, IN_PROGRESS, DONE, BLOCKED). This provides type-safe state management.
  • TaskPriority: Defines the urgency levels for a task (LOW, MEDIUM, HIGH, CRITICAL), which is useful for sorting and filtering.

Data Classes

These classes are primarily simple data containers with minimal logic.

User

Represents an actor in the system.

User

Identified by a unique ID, name, and email. A User can create or be assigned to tasks.

Tag

Tag

A simple wrapper for a string that categorizes tasks (e.g., "feature", "bug"). Tags are reusable across multiple tasks.

Comment

Stores a piece of text content, the User who authored it, and a timestamp.

Comment

It provides a mechanism for discussion on a task.

ActivityLog

Records a single change made to a task with a description and a timestamp, creating an immutable audit trail.

ActivityLog

Core Classes

These classes encapsulate the main business logic and behavior of the system.

Task

The central entity of the system. It aggregates all relevant information like title, description, due date, priority, assignee, comments, and subtasks. It also manages its own state transitions and notifies observers of changes.

TaskList

A container for a logical grouping of Task objects (e.g., a project's backlog or a sprint's tasks).

TaskManagementSystem

The main controller and entry point for the application. It acts as a central repository for all users, tasks, and lists, providing a simplified interface for all system operations.

3.2 Class Relationships

The classes interact through several well-defined relationships, which helps in maintaining low coupling and high cohesion.

Composition (Strong "has-a")

  • TaskManagementSystem has a Map of User, Task, and TaskList. The system owns and manages the lifecycle of these core objects.
  • TaskList has a List of Tasks. The tasks belong to the list.
  • Task has a List of Comments and ActivityLogs. These are integral parts of a task and do not exist independently.
  • Task has a List of Tasks (subtasks), forming a recursive composition.

Association (Weak "has-a" / "uses-a")

  • A Task is associated with a User as its assignee and createdBy. The User is an independent entity. This is a one-to-many relationship (one User can be assigned many Tasks).
  • A Comment is associated with a User (its author).
  • A Task is associated with a TaskState object, to which it delegates state-specific behavior.

Aggregation ("has-a")

  • Task has a Set of Tags. A Tag like "bug" can exist on its own and be associated with multiple tasks.
  • Task has a list of TaskObservers. The observers are independent entities that register with the task.

Realization/Implementation ("is-a")

This relationship exists between an interface and the concrete classes that implement it.

  • TodoState, InProgressState, and DoneState classes implement the TaskState interface.
  • SortByPriority and SortByDueDate classes implement the TaskSortStrategy interface.
  • ActivityLogger implements the TaskObserver interface.

3.3 Key Design Patterns

Several design patterns are employed to ensure the system is flexible, scalable, and maintainable.

State Pattern

TaskState

The TaskState interface and its concrete implementations (TodoState, InProgressState, DoneState) manage the lifecycle of a Task. This pattern isolates state-specific logic, making it easy to add new states (e.g., BlockedState) without modifying the Task class.

Strategy Pattern

TaskSortStrategy

The TaskSortStrategy interface allows different sorting algorithms (SortByPriority, SortByDueDate) to be encapsulated and used interchangeably. The client (TaskManagementSystem) can select a sorting strategy at runtime.

Observer Pattern

TaskObserver

The TaskObserver pattern is used to notify dependent objects (ActivityLogger) of any change in a Task's state. This decouples the Task (the subject) from the objects that need to react to its changes (the observers).

Composite Pattern

The Task class can contain a collection of other Task objects (subtasks). This allows a client to treat an individual task and a group of tasks (a task with its subtasks) uniformly, as seen in the recursive display() method.

Builder Pattern

The Task class uses a nested static TaskBuilder to construct complex Task objects. This pattern handles numerous optional parameters cleanly and improves the readability of object creation.

Facade Pattern

The TaskManagementSystem also serves as a Facade, providing a simple, unified interface (createTask, createUser, etc.) to the more complex underlying subsystem of creating and linking tasks, users, builders, and observers.

Singleton Pattern

The TaskManagementSystem class is implemented as a Singleton to ensure there is only one instance of it acting as the central point of control and data repository for the entire application.

3.4 Full Class Diagram

Tasks Management System Class Diag

4. Implementation

4.1 Enums: TaskStatus and TaskPriority

1class TaskStatus(Enum):
2    TODO = "TODO"
3    IN_PROGRESS = "IN_PROGRESS"
4    DONE = "DONE"
5    BLOCKED = "BLOCKED"
6
7class TaskPriority(Enum):
8    LOW = "LOW"
9    MEDIUM = "MEDIUM"
10    HIGH = "HIGH"
11    CRITICAL = "CRITICAL"

These enums define task metadata:

  • TaskStatus tracks task lifecycle stages.
  • TaskPriority indicates the urgency of the task, used for sorting and filtering.

4.2 User

Represents a user in the system with a unique ID, name, and email. Used as creator or assignee for tasks.

1class User:
2    def __init__(self, name: str, email: str):
3        self._id = str(uuid.uuid4())
4        self._name = name
5        self._email = email
6    
7    @property
8    def id(self) -> str:
9        return self._id
10    
11    @property
12    def email(self) -> str:
13        return self._email
14    
15    @property
16    def name(self) -> str:
17        return self._name

4.3 Tag

Represents a tag (e.g., "bug", "feature") used to categorize tasks. Tags are optional metadata.

1class Tag:
2    def __init__(self, name: str):
3        self._name = name
4    
5    @property
6    def name(self) -> str:
7        return self._name

4.4 Comment

Models a comment left on a task by a user, with timestamp for audit trail.

1class Comment:
2    def __init__(self, content: str, author: User):
3        self._id = str(uuid.uuid4())
4        self._content = content
5        self._author = author
6        self._timestamp = datetime.now()
7    
8    @property
9    def author(self) -> User:
10        return self._author

4.5 ActivityLog

Tracks updates to a task like status changes, comments, assignment, etc. Used for task history and accountability.

1class ActivityLog:
2    def __init__(self, description: str):
3        self._description = description
4        self._timestamp = datetime.now()
5    
6    def __str__(self) -> str:
7        return f"[{self._timestamp}] {self._description}"

4.6 Task

The Task class is the heart of the system. It's a complex entity that aggregates data and behavior to manage its state, relationships, and history.

1class Task:
2    def __init__(self, builder: 'TaskBuilder'):
3        self._id = builder._id
4        self._title = builder._title
5        self._description = builder._description
6        self._due_date = builder._due_date
7        self._priority = builder._priority
8        self._created_by = builder._created_by
9        self._assignee = builder._assignee
10        self._tags = builder._tags
11        self._current_state = TodoState()  # Initial state
12        self._comments: List[Comment] = []
13        self._subtasks: List['Task'] = []
14        self._activity_logs: List[ActivityLog] = []
15        self._observers: List[TaskObserver] = []
16        self._lock = threading.Lock()
17        self.add_log(f"Task created with title: {self._title}")
18    
19    def set_assignee(self, user: User):
20        with self._lock:
21            self._assignee = user
22            self.add_log(f"Assigned to {user.name}")
23            self.notify_observers("assignee")
24    
25    def update_priority(self, priority: TaskPriority):
26        with self._lock:
27            self._priority = priority
28            self.notify_observers("priority")
29    
30    def add_comment(self, comment: Comment):
31        with self._lock:
32            self._comments.append(comment)
33            self.add_log(f"Comment added by {comment.author.name}")
34            self.notify_observers("comment")
35    
36    def add_subtask(self, subtask: 'Task'):
37        with self._lock:
38            self._subtasks.append(subtask)
39            self.add_log(f"Subtask added: {subtask.get_title()}")
40            self.notify_observers("subtask_added")
41    
42    # State Pattern Methods
43    def set_state(self, state: TaskState):
44        self._current_state = state
45        self.add_log(f"Status changed to: {state.get_status().value}")
46        self.notify_observers("status")
47    
48    def start_progress(self):
49        self._current_state.start_progress(self)
50    
51    def complete_task(self):
52        self._current_state.complete_task(self)
53    
54    def reopen_task(self):
55        self._current_state.reopen_task(self)
56    
57    # Observer Pattern Methods
58    def add_observer(self, observer: TaskObserver):
59        self._observers.append(observer)
60    
61    def remove_observer(self, observer: TaskObserver):
62        if observer in self._observers:
63            self._observers.remove(observer)
64    
65    def notify_observers(self, change_type: str):
66        for observer in self._observers:
67            observer.update(self, change_type)
68    
69    def add_log(self, log_description: str):
70        self._activity_logs.append(ActivityLog(log_description))
71    
72    def is_composite(self) -> bool:
73        return len(self._subtasks) > 0
74    
75    def display(self, indent: str = ""):
76        print(f"{indent}- {self._title} [{self.get_status().value}, {self._priority.value}, Due: {self._due_date}]")
77        if self.is_composite():
78            for subtask in self._subtasks:
79                subtask.display(indent + "  ")
80    
81    # Getters and setters
82    def get_id(self) -> str:
83        return self._id
84    
85    def get_title(self) -> str:
86        return self._title
87    
88    def get_description(self) -> str:
89        return self._description
90    
91    def get_priority(self) -> TaskPriority:
92        return self._priority
93    
94    def get_due_date(self) -> date:
95        return self._due_date
96    
97    def get_assignee(self) -> Optional[User]:
98        return self._assignee
99    
100    def set_title(self, title: str):
101        self._title = title
102    
103    def set_description(self, description: str):
104        self._description = description
105    
106    def get_status(self) -> TaskStatus:
107        return self._current_state.get_status()
108    
109    # Builder Pattern
110    class TaskBuilder:
111        def __init__(self, title: str):
112            self._id = str(uuid.uuid4())
113            self._title = title
114            self._description = ""
115            self._due_date = None
116            self._priority = None
117            self._created_by = None
118            self._assignee = None
119            self._tags = set()
120        
121        def description(self, description: str) -> 'Task.TaskBuilder':
122            self._description = description
123            return self
124        
125        def due_date(self, due_date: date) -> 'Task.TaskBuilder':
126            self._due_date = due_date
127            return self
128        
129        def priority(self, priority: TaskPriority) -> 'Task.TaskBuilder':
130            self._priority = priority
131            return self
132        
133        def assignee(self, assignee: User) -> 'Task.TaskBuilder':
134            self._assignee = assignee
135            return self
136        
137        def created_by(self, created_by: User) -> 'Task.TaskBuilder':
138            self._created_by = created_by
139            return self
140        
141        def tags(self, tags: Set[Tag]) -> 'Task.TaskBuilder':
142            self._tags = tags
143            return self
144        
145        def build(self) -> 'Task':
146            return Task(self)

A task has many optional and required attributes, making its constructor complex. The Builder Pattern provides a clean, fluent API for creating Task objects.

4.7 TaskList

1class TaskList:
2    def __init__(self, name: str):
3        self._id = str(uuid.uuid4())
4        self._name = name
5        self._tasks: List[Task] = []
6        self._lock = threading.Lock()
7    
8    def add_task(self, task: Task):
9        with self._lock:
10            self._tasks.append(task)
11    
12    def get_tasks(self) -> List[Task]:
13        with self._lock:
14            return self._tasks.copy()  # Return a copy to prevent external modification
15    
16    @property
17    def id(self) -> str:
18        return self._id
19    
20    @property
21    def name(self) -> str:
22        return self._name
23    
24    def display(self):
25        print(f"--- Task List: {self._name} ---")
26        for task in self._tasks:
27            task.display("")
28        print("-----------------------------------")

Encapsulates a logical group of tasks (e.g., “Bugs”, “Features”). Allows displaying grouped tasks in a hierarchical view.

4.8 Task Sorting Strategies

Implements the Strategy pattern to allow pluggable sorting mechanisms. New strategies (e.g., by creation date or assignee) can be added easily.

1class TaskSortStrategy(ABC):
2    @abstractmethod
3    def sort(self, tasks: List[Task]):
4        pass
5
6class SortByPriority(TaskSortStrategy):
7    def sort(self, tasks: List[Task]):
8        # Higher priority comes first (CRITICAL > HIGH > MEDIUM > LOW)
9        priority_order = {TaskPriority.CRITICAL: 4, TaskPriority.HIGH: 3, 
10                         TaskPriority.MEDIUM: 2, TaskPriority.LOW: 1}
11        tasks.sort(key=lambda task: priority_order.get(task.get_priority(), 0), reverse=True)
12
13class SortByDueDate(TaskSortStrategy):
14    def sort(self, tasks: List[Task]):
15        tasks.sort(key=lambda task: task.get_due_date() if task.get_due_date() else date.max)

4.9 TaskObserver

1class TaskObserver(ABC):
2    @abstractmethod
3    def update(self, task: 'Task', change_type: str):
4        pass
5
6class ActivityLogger(TaskObserver):
7    def update(self, task: 'Task', change_type: str):
8        print(f"LOGGER: Task '{task.get_title()}' was updated. Change: {change_type}")

External components like loggers or notification systems can subscribe to task changes without being tightly coupled.

4.10 TaskState

1class TaskState(ABC):
2    @abstractmethod
3    def start_progress(self, task: 'Task'):
4        pass
5    
6    @abstractmethod
7    def complete_task(self, task: 'Task'):
8        pass
9    
10    @abstractmethod
11    def reopen_task(self, task: 'Task'):
12        pass
13    
14    @abstractmethod
15    def get_status(self) -> TaskStatus:
16        pass
17
18class TodoState(TaskState):
19    def start_progress(self, task: 'Task'):
20        task.set_state(InProgressState())
21    
22    def complete_task(self, task: 'Task'):
23        print("Cannot complete a task that is not in progress.")
24    
25    def reopen_task(self, task: 'Task'):
26        print("Task is already in TO-DO state.")
27    
28    def get_status(self) -> TaskStatus:
29        return TaskStatus.TODO
30
31class InProgressState(TaskState):
32    def start_progress(self, task: 'Task'):
33        print("Task is already in progress.")
34    
35    def complete_task(self, task: 'Task'):
36        task.set_state(DoneState())
37    
38    def reopen_task(self, task: 'Task'):
39        task.set_state(TodoState())
40    
41    def get_status(self) -> TaskStatus:
42        return TaskStatus.IN_PROGRESS
43
44class DoneState(TaskState):
45    def start_progress(self, task: 'Task'):
46        print("Cannot start a completed task. Reopen it first.")
47    
48    def complete_task(self, task: 'Task'):
49        print("Task is already done.")
50    
51    def reopen_task(self, task: 'Task'):
52        task.set_state(TodoState())
53    
54    def get_status(self) -> TaskStatus:
55        return TaskStatus.DONE

Each TaskState implementation (TodoState, InProgressState, DoneState) knows which transitions are valid from its current state. This design cleans up the Task class and makes it easy to add new states (e.g., BlockedState) without modifying existing code.

4.11 TaskManagementSystem

This class serves as the central point of control. It uses the Singleton Pattern to ensure a single instance manages all system data and the Facade Pattern to provide a simple, unified interface to the complex subsystem.

1class TaskManagementSystem:
2    _instance = None
3    _lock = threading.Lock()
4    
5    def __new__(cls):
6        if cls._instance is None:
7            with cls._lock:
8                if cls._instance is None:
9                    cls._instance = super().__new__(cls)
10                    cls._instance._initialized = False
11        return cls._instance
12    
13    def __init__(self):
14        if not self._initialized:
15            self._users: Dict[str, User] = {}
16            self._tasks: Dict[str, Task] = {}
17            self._task_lists: Dict[str, TaskList] = {}
18            self._initialized = True
19    
20    @classmethod
21    def get_instance(cls):
22        return cls()
23    
24    def create_user(self, name: str, email: str) -> User:
25        user = User(name, email)
26        self._users[user.id] = user
27        return user
28    
29    def create_task_list(self, list_name: str) -> TaskList:
30        task_list = TaskList(list_name)
31        self._task_lists[task_list.id] = task_list
32        return task_list
33    
34    def create_task(self, title: str, description: str, due_date: date,
35                   priority: TaskPriority, created_by_user_id: str) -> Task:
36        created_by = self._users.get(created_by_user_id)
37        if created_by is None:
38            raise ValueError("User not found.")
39        
40        task = Task.TaskBuilder(title) \
41            .description(description) \
42            .due_date(due_date) \
43            .priority(priority) \
44            .created_by(created_by) \
45            .build()
46        
47        task.add_observer(ActivityLogger())
48        
49        self._tasks[task.get_id()] = task
50        return task
51    
52    def list_tasks_by_user(self, user_id: str) -> List[Task]:
53        user = self._users.get(user_id)
54        return [task for task in self._tasks.values() 
55                if task.get_assignee() == user]
56    
57    def list_tasks_by_status(self, status: TaskStatus) -> List[Task]:
58        return [task for task in self._tasks.values() 
59                if task.get_status() == status]
60    
61    def delete_task(self, task_id: str):
62        if task_id in self._tasks:
63            del self._tasks[task_id]
64    
65    def search_tasks(self, keyword: str, sorting_strategy: TaskSortStrategy) -> List[Task]:
66        matching_tasks = []
67        for task in self._tasks.values():
68            if (keyword in task.get_title() or 
69                keyword in task.get_description()):
70                matching_tasks.append(task)
71        
72        sorting_strategy.sort(matching_tasks)
73        return matching_tasks
  • Singleton: Ensures that there is only one central repository for all users, tasks, and lists throughout the application.
  • Facade: Hides the complexity of creating and linking objects. A client simply calls taskManagementSystem.createTask(...) without needing to know about builders, observers, or internal storage maps.

4.12 TaskManagementSystem Demo

The TaskManagementSystemDemo class demonstrates how a client would interact with the system's facade to perform common operations.

1class TaskManagementSystemDemo:
2    @staticmethod
3    def main():
4        task_management_system = TaskManagementSystem.get_instance()
5        
6        # Create users
7        user1 = task_management_system.create_user("John Doe", "[email protected]")
8        user2 = task_management_system.create_user("Jane Smith", "[email protected]")
9        
10        # Create task lists
11        task_list1 = task_management_system.create_task_list("Enhancements")
12        task_list2 = task_management_system.create_task_list("Bug Fix")
13        
14        # Create tasks
15        task1 = task_management_system.create_task(
16            "Enhancement Task", "Launch New Feature",
17            date.today().replace(day=date.today().day + 2), 
18            TaskPriority.LOW, user1.id
19        )
20        subtask1 = task_management_system.create_task(
21            "Enhancement sub task", "Design UI/UX",
22            date.today().replace(day=date.today().day + 1), 
23            TaskPriority.MEDIUM, user1.id
24        )
25        task2 = task_management_system.create_task(
26            "Bug Fix Task", "Fix API Bug",
27            date.today().replace(day=date.today().day + 3), 
28            TaskPriority.HIGH, user2.id
29        )
30        
31        task1.add_subtask(subtask1)
32        
33        task_list1.add_task(task1)
34        task_list2.add_task(task2)
35        
36        task_list1.display()
37        
38        # Update task status
39        subtask1.start_progress()
40        
41        # Assign task
42        subtask1.set_assignee(user2)
43        
44        task_list1.display()
45        
46        # Search tasks
47        search_results = task_management_system.search_tasks("Task", SortByDueDate())
48        print("\nTasks with keyword Task:")
49        for task in search_results:
50            print(task.get_title())
51        
52        # Filter tasks by status
53        filtered_tasks = task_management_system.list_tasks_by_status(TaskStatus.TODO)
54        print("\nTODO Tasks:")
55        for task in filtered_tasks:
56            print(task.get_title())
57        
58        # Mark a task as done
59        subtask1.complete_task()
60        
61        # Get tasks assigned to a user
62        user_task_list = task_management_system.list_tasks_by_user(user2.id)
63        print(f"\nTask for {user2.name}:")
64        for task in user_task_list:
65            print(task.get_title())
66        
67        task_list1.display()
68        
69        # Delete a task
70        task_management_system.delete_task(task2.get_id())
71
72if __name__ == "__main__":
73    TaskManagementSystemDemo.main()

This driver code simulates a user's interaction with the system, showcasing:

  • Object creation with the Builder.
  • Hierarchical task management via the Composite pattern.
  • State transitions managed by the State pattern.
  • Automatic logging via the Observer pattern.
  • Flexible searching and sorting using the Strategy pattern.

5. Run and Test

Languages
Java
C#
Python
C++
Files19
entities
enums
observers
states
strategies
task_management_system_demo.py
main
task_management_system.py
task_management_system_demo.py
Output

6. Quiz

Design Task Management System Quiz

1 / 21
Multiple Choice

Which is the most appropriate entity to log changes such as task creation or status updates in a task management system design?

How helpful was this article?

Comments (3)


0/2000
Sort by
The Cap17 days ago

In TaskManagementSystem you are just creating taskList, where as while creating task you are never assigning it to any task list. So please fix it as it is supporting the functional requirement

Rishabh Raj16 days ago
taskList1->addTask(task1);
he seems to be doing that with this right? what i mean is the created tasks are appended to this tasklist list/vector.
Rishabh Raj18 days ago

task management system can have probably a double checked locking or static instance as i think static instance are thread safe now.

Copilot extension content script